﻿using System.Collections.Generic;
using System.Linq.Expressions;

namespace System.Linq
{
    public static class ExpressionTreeBuilder
    {
        private static Expression<Func<TVal, bool>> BuildExpressionTree<TVal, TPar>(ExpressionType type, IEnumerable<TPar> parameters,
            Expression<Func<TVal, TPar, bool>> predicate, Func<string, TPar, object> parResolver)
        {
            var expression = predicate.Body as BinaryExpression;
            if (expression == null)
                throw new ArgumentException("Выражение имеет неправильный вид");
            var expressions = new List<BinaryExpression>(parameters.Count());
            foreach (var id in parameters)
                expressions.Add(ProcessBinaryExpression(expression, id, parResolver));
            return Expression.Lambda<Func<TVal, bool>>(BuildExpression(type, expressions), predicate.Parameters.First(p => p.Type == typeof(TVal)));
        }

        private static BinaryExpression ProcessBinaryExpression<TPar>(BinaryExpression expression, TPar parameter, Func<string, TPar, object> parResolver)
        {
            Expression leftExpression;
            Expression rightExpression;
            if (expression.Left is MemberExpression && expression.Right is MemberExpression)
            {
                var left = (MemberExpression)expression.Left;
                var right = (MemberExpression)expression.Right;
                if (left.Member.ReflectedType == typeof(TPar))
                {
                    rightExpression = expression.Right;
                    leftExpression = Expression.Constant(GetValue(left.Member.Name, parameter, parResolver));
                }
                else if (right.Member.ReflectedType == typeof(TPar))
                {
                    leftExpression = expression.Left;
                    rightExpression = Expression.Constant(GetValue(right.Member.Name, parameter, parResolver));
                }
                else
                {
                    rightExpression = expression.Right;
                    leftExpression = expression.Left;
                }
            }
            else if ((expression.Left is UnaryExpression && expression.Right is MemberExpression) || (expression.Left is MemberExpression && expression.Right is UnaryExpression))
            {
                if (expression.Right is UnaryExpression)
                {
                    var right = (UnaryExpression)expression.Right;
                    if (right.Operand is MemberExpression)
                    {
                        var rightOperand = (MemberExpression)right.Operand;
                        if (rightOperand.Member.ReflectedType == typeof(TPar))
                            rightExpression = Expression.MakeUnary(right.NodeType, Expression.Constant(GetValue(rightOperand.Member.Name, parameter, parResolver)), right.Type);
                        else
                            rightExpression = expression.Right;
                    }
                    else
                        rightExpression = expression.Right;
                    leftExpression = expression.Left;
                }
                else if (expression.Left is UnaryExpression)
                {
                    var left = (UnaryExpression)expression.Left;
                    if (left.Operand is MemberExpression)
                    {
                        var leftOperand = (MemberExpression)left.Operand;
                        if (leftOperand.Member.ReflectedType == typeof(TPar))
                            leftExpression = Expression.MakeUnary(left.NodeType, Expression.Constant(GetValue(leftOperand.Member.Name, parameter, parResolver)), left.Type);
                        else
                            leftExpression = expression.Left;
                    }
                    else
                        leftExpression = expression.Left;
                    rightExpression = expression.Right;
                }
                else
                {
                    rightExpression = expression.Right;
                    leftExpression = expression.Left;
                }
            }
            else if (expression.Left is BinaryExpression && expression.Right is BinaryExpression)
            {
                leftExpression = ProcessBinaryExpression((BinaryExpression)expression.Left, parameter, parResolver);
                rightExpression = ProcessBinaryExpression((BinaryExpression)expression.Right, parameter, parResolver);
            }
            else
            {
                leftExpression = expression.Left;
                rightExpression = expression.Right;
            }
            return Expression.MakeBinary(expression.NodeType, leftExpression, rightExpression);
        }

        private static object GetValue<TPar>(string propName, TPar parameter, Func<string, TPar, object> parResolver)
        {
            object res = parResolver == null ? null : parResolver(propName, parameter);
            if (res == null)
                res = parameter.GetType().GetProperty(propName).GetValue(parameter, null);
            return res;
        }

        private static BinaryExpression BuildExpression(ExpressionType type, List<BinaryExpression> expressions)
        {
            if (expressions.Count == 0)
                return Expression.Equal(Expression.Constant(0), Expression.Constant(1)); //Если выражений нет, то и запрос должен вернуть пустой список
            else if (expressions.Count == 1)
                return expressions[0];
            var center = expressions.Count / 2;
            return Expression.MakeBinary(type,
                BuildExpression(type, expressions.Take(center).ToList()),
                BuildExpression(type, expressions.Skip(center).Take(expressions.Count - center).ToList()));
        }

        #region Extensions

        /// <summary>
        /// Создает условие Where по переданному списку идентификаторов. Если список идентификаторов пуст, создается заведомо невыполнимое условие.
        /// </summary>
        /// <typeparam name="T">Entity</typeparam>
        /// <param name="source">EF IQueryable</param>
        /// <param name="type">Как объединять выражения (and, or, и т.д.)</param>
        /// <param name="parameters">Список идентификаторов</param>
        /// <param name="predicate">Выражение, в которое будут подставляться значения из списка идентификаторов</param>
        /// <returns></returns>
        public static IQueryable<T> Where<T>(this IQueryable<T> source, ExpressionType type, IEnumerable<Identifier> parameters,
            Expression<Func<T, Identifier, bool>> predicate)
        {
            return source.Where(type, parameters, predicate, (pn, o) =>
            {
                switch (pn)
                {
                    case "Id":
                        return o.Id;
                    case "Revision":
                        return o.Revision;
                    default:
                        return null;
                }
            });
        }

        /// <summary>
        /// Создает условие Where по переданному списку значений. Если список значений пуст, создается заведомо невыполнимое условие.
        /// </summary>
        /// <typeparam name="T">Entity</typeparam>
        /// <typeparam name="P">Тип параметров</typeparam>
        /// <param name="source">EF IQueryable</param>
        /// <param name="type">Как объединять выражения (and, or, и т.д.)</param>
        /// <param name="parameters">Список из которого будут браться значения для выражений</param>
        /// <param name="predicate">Выражение, в которое будут подставляться значения из списка</param>
        /// <param name="parResolver">Делегат, возвращающий значение поля по его имени. Если возвращается null, то для получения значения используется reflection</param>
        /// <returns></returns>
        public static IQueryable<T> Where<T, P>(this IQueryable<T> source, ExpressionType type, IEnumerable<P> parameters,
            Expression<Func<T, P, bool>> predicate, Func<string, P, object> parResolver)
        {
            var p = BuildExpressionTree<T, P>(type, parameters, predicate, parResolver);
            if (p != null)
                return source.Where(p);
            return source;
        }

        #endregion
    }
}
